// @TODO: It seems that SVG filters are pretty expensive for resvg, PNG
// generation time 10x'd when adding this filter (WASM in browser).
// https://drafts.fxtf.org/filter-effects/#feGaussianBlurElement

import { buildXMLString } from '../utils.js'

function shiftPath(path: string, dx: number, dy: number) {
  return path.replace(
    /([MA])([0-9.-]+),([0-9.-]+)/g,
    function (_, command, x, y) {
      return command + (parseFloat(x) + dx) + ',' + (parseFloat(y) + dy)
    }
  )
}

// The scale is used to make the filter area larger than the bounding box,
// because usually the given measured text bounding is larger than the path
// bounding.
// The text bounding box is measured via the font metrics, which is not the same
// as the actual content. For example, the text bounding box of "A" is larger
// than the actual "a" path but they have the same font metrics.
// This scale can be adjusted to prevent the filter from cutting off the text.
const SCALE = 1.1

export function buildDropShadow(
  { id, width, height }: { id: string; width: number; height: number },
  style: {
    shadowColor: string[]
    shadowOffset: {
      width: number
      height: number
    }[]
    shadowRadius: number[]
  }
) {
  if (
    !style.shadowColor ||
    !style.shadowOffset ||
    typeof style.shadowRadius === 'undefined'
  ) {
    return ''
  }

  const shadowCount = style.shadowColor.length
  let effects = ''
  let merge = ''

  // There could be multiple shadows, we need to get the maximum bounding box
  // and use `feMerge` to merge them together.
  let left = 0
  let right = width
  let top = 0
  let bottom = height
  for (let i = 0; i < shadowCount; i++) {
    // Expand the area for the filter to prevent it from cutting off.
    const grow = (style.shadowRadius[i] * style.shadowRadius[i]) / 4
    left = Math.min(style.shadowOffset[i].width - grow, left)
    right = Math.max(style.shadowOffset[i].width + grow + width, right)
    top = Math.min(style.shadowOffset[i].height - grow, top)
    bottom = Math.max(style.shadowOffset[i].height + grow + height, bottom)

    effects += buildXMLString('feDropShadow', {
      dx: style.shadowOffset[i].width,
      dy: style.shadowOffset[i].height,
      stdDeviation:
        // According to the spec, we use the half of the blur radius as the standard
        // deviation for the filter.
        // > the image that would be generated by applying to the shadow a Gaussian
        // > blur with a standard deviation equal to half the blur radius
        // > https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
        style.shadowRadius[i] / 2,
      'flood-color': style.shadowColor[i],
      'flood-opacity': 1,
      ...(shadowCount > 1
        ? {
            in: 'SourceGraphic',
            result: `satori_s-${id}-result-${i}`,
          }
        : {}),
    })

    if (shadowCount > 1) {
      // Merge needs to be in reverse order.
      merge =
        buildXMLString('feMergeNode', {
          in: `satori_s-${id}-result-${i}`,
        }) + merge
    }
  }

  return buildXMLString(
    'filter',
    {
      id: `satori_s-${id}`,
      x: ((left / width) * 100 * SCALE).toFixed(2) + '%',
      y: ((top / height) * 100 * SCALE).toFixed(2) + '%',
      width: (((right - left) / width) * 100 * SCALE).toFixed(2) + '%',
      height: (((bottom - top) / height) * 100 * SCALE).toFixed(2) + '%',
    },
    effects + (merge ? buildXMLString('feMerge', {}, merge) : '')
  )
}

export function boxShadow(
  {
    width,
    height,
    shape,
    opacity,
    id,
  }: {
    width: number
    height: number
    shape: string
    opacity: number
    id: string
  },
  style: Record<string, any>
) {
  if (!style.boxShadow) return null

  let shadow = ''
  let innerShadow = ''

  for (let i = style.boxShadow.length - 1; i >= 0; i--) {
    let s = ''

    const shadowStyle = style.boxShadow[i]

    if (shadowStyle.spreadRadius && shadowStyle.inset) {
      shadowStyle.spreadRadius = -shadowStyle.spreadRadius
    }

    // Expand the area for the filter to prevent it from cutting off.
    const grow =
      (shadowStyle.blurRadius * shadowStyle.blurRadius) / 4 +
      (shadowStyle.spreadRadius || 0)

    const left = Math.min(
      -grow - (shadowStyle.inset ? shadowStyle.offsetX : 0),
      0
    )
    const right = Math.max(
      grow + width - (shadowStyle.inset ? shadowStyle.offsetX : 0),
      width
    )
    const top = Math.min(
      -grow - (shadowStyle.inset ? shadowStyle.offsetY : 0),
      0
    )
    const bottom = Math.max(
      grow + height - (shadowStyle.inset ? shadowStyle.offsetY : 0),
      height
    )

    const sid = `satori_s-${id}-${i}`
    const maskId = `satori_ms-${id}-${i}`
    const shapeWithSpread = shadowStyle.spreadRadius
      ? shape.replace(
          'stroke-width="0"',
          `stroke-width="${shadowStyle.spreadRadius * 2}"`
        )
      : shape

    s += buildXMLString(
      'mask',
      {
        id: maskId,
        maskUnits: 'userSpaceOnUse',
      },
      buildXMLString('rect', {
        x: 0,
        y: 0,
        width: style._viewportWidth || '100%',
        height: style._viewportHeight || '100%',
        fill: shadowStyle.inset ? '#000' : '#fff',
      }) +
        shapeWithSpread
          .replace(
            'fill="#fff"',
            shadowStyle.inset ? 'fill="#fff"' : 'fill="#000"'
          )
          .replace('stroke="#fff"', '')
    )

    let finalShape = shapeWithSpread
      .replace(/d="([^"]+)"/, (_, path) => {
        return (
          'd="' +
          shiftPath(path, shadowStyle.offsetX, shadowStyle.offsetY) +
          '"'
        )
      })
      .replace(/x="([^"]+)"/, (_, x) => {
        return 'x="' + (parseFloat(x) + shadowStyle.offsetX) + '"'
      })
      .replace(/y="([^"]+)"/, (_, y) => {
        return 'y="' + (parseFloat(y) + shadowStyle.offsetY) + '"'
      })

    // Negative spread radius, we need another mask here.
    if (shadowStyle.spreadRadius && shadowStyle.spreadRadius < 0) {
      s += buildXMLString(
        'mask',
        {
          id: maskId + '-neg',
          maskUnits: 'userSpaceOnUse',
        },
        finalShape
          .replace('stroke="#fff"', 'stroke="#000"')
          .replace(
            /stroke-width="[^"]+"/,
            `stroke-width="${-shadowStyle.spreadRadius * 2}"`
          )
      )
    }

    if (shadowStyle.spreadRadius && shadowStyle.spreadRadius < 0) {
      finalShape = buildXMLString(
        'g',
        {
          mask: `url(#${maskId}-neg)`,
        },
        finalShape
      )
    }

    s +=
      buildXMLString(
        'defs',
        {},
        buildXMLString(
          'filter',
          {
            id: sid,
            x: `${(left / width) * 100}%`,
            y: `${(top / height) * 100}%`,
            width: `${((right - left) / width) * 100}%`,
            height: `${((bottom - top) / height) * 100}%`,
          },
          buildXMLString('feGaussianBlur', {
            // According to the spec, we use the half of the blur radius as the standard
            // deviation for the filter.
            // > the image that would be generated by applying to the shadow a Gaussian
            // > blur with a standard deviation equal to half the blur radius
            // > https://www.w3.org/TR/css-backgrounds-3/#shadow-blur
            stdDeviation: shadowStyle.blurRadius / 2,
            result: 'b',
          }) +
            buildXMLString('feFlood', {
              'flood-color': shadowStyle.color,
              in: 'SourceGraphic',
              result: 'f',
            }) +
            buildXMLString('feComposite', {
              in: 'f',
              in2: 'b',
              operator: shadowStyle.inset ? 'out' : 'in',
            })
        )
      ) +
      buildXMLString(
        'g',
        {
          mask: `url(#${maskId})`,
          filter: `url(#${sid})`,
          opacity: opacity,
        },
        finalShape
      )

    if (shadowStyle.inset) {
      innerShadow += s
    } else {
      shadow += s
    }
  }

  return [shadow, innerShadow]
}
